Stats Tweet

Calcolatore, Linguaggio del.

Ogni calcolatore è in grado di comprendere ed eseguire un certo numero di istruzioni quando queste sono codificate nel suo specifico linguaggio macchina. Solitamente le operazioni fornite riguardano: trasferimento di dati, elaborazione di dati (aritmetiche, logiche), test e salto, controllo, ma è possibile trovare, grazie alla maggior scala di integrazione, istruzioni per i calcoli grafici - prodotto scalare - o la gestione della memoria - memoria virtuale, memoria protetta. Istruzioni e dati del linguaggio macchina vanno espressi in formato binario, ma programmi appositi - gli assemblatori - consentono di usare nomi mnemonici (facili da ricordare) invece che numeri; sarà poi l'assemblatore a tradurre il linguaggio assembly in numeri segnalandoci i nomi errati. Anche la programmazione in assembly non è molto agevole, poiché le istruzioni sono a basso livello, e ciò obbliga a usarne molte solo per esprimere una semplice operazione: la somma C=A+B richiede di copiare A e B in 2 registri, sommare e porre il risultato in un terzo registro e da lì copiarlo in C. Già dalla seconda generazione di calcolatori sono stati perciò scritti dei programmi - i compilatori - in grado di tradurre un programma sorgente, scritto in un linguaggio mnemonico di livello più alto dell'assembly e tale da esprimere concisamente certe operazioni specifiche, in un programma oggetto. Una volta compilato, il nostro programma può essere direttamente eseguito dal calcolatore senza altri intermediari; se prevediamo di usarlo più volte, potremo memorizzarlo ed in seguito ricaricarlo senza passare ancora per il compilatore. Un tipo particolare di compilatore è l'interprete, che traduce ed esegue subito ogni singola istruzione: da un lato ci permette quindi di verificare gli effetti di un programma durante la stesura; dall'altro la continua traduzione rallenta l'esecuzione. Esistono poi interpreti/compilatori con cui dapprima si mette a punto il programma e poi lo si compila così da ottenere la massima velocità. Una tecnica di programmazione molto usata è la procedura, in varie forme: si tratta di una sequenza di istruzioni a cui possiamo riferirci con un nome - chiamata di procedura - passando dei valori - argomenti - così da eseguire la sequenza senza doverla riscrivere. Il programma viene suddiviso in procedure, e dato che ognuna di esse ha un nome significativo, noi possiamo vedere a colpo d'occhio l'azione di tutto il programma. Una procedura che abbia un solo risultato e non modifichi nessun argomento è una funzione matematica; ovviamente esistono anche procedure senza valori di ritorno o che ritornano più valori alterando i loro argomenti. Nella sequenza di istruzioni di una procedura può anche trovarsi una chiamata ricorsiva: si tratta di un nuovo riferimento alla nostra procedura, ma con argomenti differenti; in altre parole vi sono 2 copie della procedura in esecuzione: la prima ha chiamato la seconda e ne attende il ritorno prima di proseguire. La ricorsività è una tecnica per operare su dati differenti conservando un legame fra di loro, costituito dalle diverse copie della procedura. L'iterazione è un'altra tecnica per eseguire un'operazione più volte, ma senza mantenere legami fra i dati, cioè con una sola copia della procedura: un'istruzione di ciclo racchiude una sequenza di istruzioni e specifica la condizione per rieseguirla (V. esempio più avanti). Generalmente la redazione di un programma inizia con la stesura di uno schema a blocchi o flow chart, nella quale le operazioni logiche che il calcolatore deve eseguire sono redatte in pseudo-linguaggio nella loro successione logica. Gli schemi a blocchi sono indipendenti dal linguaggio simbolico usato e naturalmente anche dal calcolatore che deve eseguire il programma. I due tipi principali di blocchi sono i seguenti: a) blocco imperativo, indicato col simbolo

CALCEO00.png


impone al calcolatore di eseguire una certa operazione; può avere più di un ingresso ma ha una sola uscita, e precisamente quella che porta all'istruzione successiva. b) blocco di test, indicato col simbolo

CALCEO01.png


ha uno o più ingressi ma sempre due uscite. In questo blocco si pone al calcolatore una domanda, alla quale esso può rispondere SI o NO; secondo la risposta che dà alla domanda il calcolatore passa a eseguire l'istruzione posta sull'uscita SI o sull'uscita NO. Si noti che non è possibile porre domande che richiedano una risposta diversa da queste e che le domande devono essere formulate in termini matematici ben precisi. Altri blocchi di forma circolare o ellittica vengono usati per indicare la lettura di dati o le operazioni di inizio e fine dei calcoli. Come esempio di schema blocchi, si voglia formulare di trovare il massimo di un certo numero di insiemi. Il procedimento usato in questi casi è quello di assumere il primo numero come massimo (max) e di confrontarlo col secondo. Il maggiore dei due viene confrontato col terzo. Il maggiore fra questi viene confrontato col quarto, e così via fino all'esaurimento dei numeri. Lo schema a blocchi di queste operazioni sarà il seguente:

CALCEO02.png


In forma più matematica potremmo considerare i numeri dati come un insieme di N numeri costruenti una successione a1, a2, ..., an; siccome la tastiera del calcolatore non fornisce gli indici, diremo A(1), A(2), ..., A(N), e indicheremo con A(I) il termine generico. Lo stesso schema a blocchi si scriverà allora in questo modo:

CALCEO03.png


Si noti che all'inizio dell'operazione compare un'istruzione che dice di leggere quanti sono i numeri, e poi di leggere il termine generico A(I) e di classificarlo al variare dell'indice; in tal modo si ottiene la lettura ordinata della successione. Si hanno poi delle istruzioni circa il trasferimento di numeri e la variazione di indici. Ad esempio l'istruzione: A(1) ¾ MAX significa "prendi il numero contenuto in A(1), e assegnalo alla variabile MAX", mentre l'istruzione I + 1 → I (ovvero I = I + 1) significa "aumenta di uno l'indice del termine generico" ovvero "prendi il numero successivo". Successivi sviluppi nei linguaggi - programmazione strutturata - hanno introdotto nuove tecniche per la rappresentazione dell'organizzazione del programma, come gli alberi algoritmici, in cui un programma è visto a blocchi scomposti su diversi livelli con sempre maggior dettaglio, fino ad arrivare alle effettive istruzioni - programmazione top-down. Comunque si è visto che, oltre a un linguaggio strutturato, la creazione di programmi complessi richiede un ambiente che ne semplifichi ed automatizzi l'aggiornamento - mantenimento -, consentendo il passaggio biunivoco fra un progetto simbolico ed il codice di programma. ║ Linguaggi simbolici: a questo punto per dare una formulazione definitiva a un problema di qualsiasi genere, occorre scegliere il linguaggio simbolico. Fra le diverse categorie di linguaggi comprendiamo: gli imperativi (procedurali), che permettono di esprimere un compito come una sequenza di chiamate di procedura che manipolano delle variabili; i funzionali (applicativi, logici), con cui definiamo delle funzioni da applicare a un insieme di dati (dominio), dai quali ricavare un risultato (codominio) - notate che l'iterazione sui dati è implicita: i linguaggi funzionali non hanno istruzioni di ciclo -; gli object-oriented, con cui simuliamo concetti e fenomeni di un sistema reale o immaginario. Un linguaggio è puro se rientra in una sola categoria; in effetti, i linguaggi più diffusi riuniscono caratteristiche di varie categorie. Vediamo ora brevemente i principali, con la categoria di appartenenza. a) Linguaggio FORTRAN (Imperativo). Il nome deriva dall'abbreviazione delle parole inglesi FORmula TRANslation cioè "traduzione di formule". È particolarmente indicato a esprimere problemi tecnici e scientifici, è disponibile su grandi e piccoli calcolatori ed ha avuto parecchie versioni, dal FORTRAN IV degli anni '50 al FORTRAN 90. b) Linguaggio COBOL (Imperativo). Il nome deriva dall'abbreviazione delle parole inglesi COmmon Business-Oriented Language, cioè "linguaggio comune orientato agli affari". È un linguaggio creato esplicitamente per esprimere in maniera semplice i problemi commerciali e amministrativi che richiedono generalmente una serie limitata di calcoli su una grande massa di dati. Nacque negli USA nel 1960 ad opera di rappresentanti di case costruttrici di calcolatori e dell'amministrazione statale. Esso si è rapidamente diffuso ed è ancora usato soprattutto a causa dell'enorme mole di codice esistente. c) Linguaggio LISP (Funzionale). Il nome deriva dall'abbreviazione delle parole inglesi LISt Processor cioè "elaboratore di liste". Fu creato nel 1958 da John McCarthy, visionario professore del MIT, per le ricerche sull'intelligenza artificiale, ed è infatti usato per l'implementazione di sistemi esperti e la comprensione del linguaggio naturale. Programmi e dati sono indistintamente rappresentati come liste e ciò ne permette la costruzione, valutazione e modifica durante l'esecuzione. d) Linguaggio Smalltalk (Object-Oriented). Il nome significa piccolo dialetto ed indica la semplicità del linguaggio e delle idee su cui si basa. Concepito nei primi anni '70 e successivamente aggiornato più volte, fa leva sui concetti di oggetto, classe, istanza, metodo, messaggio per costruire un intero ambiente operativo autosufficiente e modificabile al volo secondo necessità. Per le sue caratteristiche dinamiche ha bisogno di velocità e memoria, e questo ha inizialmente frenato la sua diffusione. e) Linguaggio Pascal (Imperativo). Il nome è stato dato da Niklaus Wirth, creatore del linguaggio, in onore di Blaise Pascal, il filosofo francese del '600. Fu sviluppato nel 1970 come strumento pedagogico per l'insegnamento della programmazione - e come tale veniva usato nei corsi - e con gli anni è stato ampliato per l'uso in programmi commerciali. Praticamente è disponibile su ogni calcolatore, data la semplicità di implementazione dei compilatori Pascal, e per questo ne vedremo ora più in dettaglio la definizione. I simboli vengono distinti in operatori, parole riservate, identificatori, numeri, stringhe di caratteri. Gli operatori sono utilizzati nelle espressioni logiche, aritmetiche, di assegnamento e di confronto: +, -, *, /, DIV, MOD, NOT, AND, OR, =, <, >, <>, <=, >=, IN, :=; il loro significato è facilmente intuibile, a parte: IN (appartenenza a insieme), DIV (divisione intera), MOD (modulo), <> (diverso da), * (prodotto), := (assegnamento). Le parole riservate e gli identificatori iniziano per lettera da a a z e proseguono con lettere - per le parole riservate - ed anche cifre - per gli identificatori. Un numero è composto di cifre da 0 a 9, con possibilmente un segno (+-), un punto (.) che fa da virgola per i numeri reali, una E per la notazione esponenziale. Una stringa è una sequenza di lettere, cifre e punteggiatura racchiusa fra apici ('). Il programma Pascal completo è diviso in 6 parti dichiarative, in stretto ordine, di cui le prime 5 facoltative: etichette, costanti, tipi, variabili, procedure e funzioni, istruzioni eseguibili. Ogni elemento usato deve essere prima dichiarato nella parte corrispondente; ad esempio, se nel nostro programma usiamo un'etichetta, dobbiamo includere la parte dichiarativa delle etichette. Per concludere l'esempio sopra trattato, vediamo come tradurre lo schema a blocchi in un programma. Supponiamo che il numero massimo di numeri da sommare con questo programma sia 1.000, e che i numeri siano reali, cioè con la virgola. Il programma per questo calcolo è il seguente:

PROGRAM TrovaMassimo;
CONST dimensione = 1000;
VAR numeri: ARRAY [1..dimensione] OF REAL;
ix: INTEGER;
max: REAL;
BEGIN
FOR ix := 1 TO dimensione DO READLN(numeri[Ix]);
max := numeri[1];
FOR ix := 2 TO dimensione DO IF max < numeri[Ix] THEN max := numeri[Ix];
WRITELN(max);
END.

Ogni programma Pascal inizia con un'istruzione PROGRAM che ne dichiara il nome, a scopo di documentazione. Per il numero di elementi usiamo la costante dimensione che rende evidente il significato delle istruzioni seguenti. numeri può contenere 1000 numeri reali, a cui si accede con un indice da 1 a 1000; ix è una semplice variabile di ciclo; max conterrà il massimo elemento - notate che è dello stesso tipo di numeri. Il blocco delle istruzioni eseguibili è racchiuso dalle parole riservate BEGIN ed END. Il primo ciclo FOR assegna a ix i valori da 1 a 1000, e per ogni valore esegue READLN, che legge dalla tastiera un numero reale e lo pone in numeri[Ix], vale a dire in numeri[1], ..., numeri[1000]. Poi a max viene assegnato il primo valore di numeri, come inizio. Il secondo ciclo FOR assegna a ix i valori da 2 a 1000 - poiché numeri[1] è già in max - e per ogni iterazione esegue IF, che controlla se max è minore di numeri[Ix] e, se così, assegna a max il nuovo massimo. Infine, WRITELN stampa su schermo il valore di max. Naturalmente questo esempio è veramente semplice; problemi più complessi vengono trattati scomponendoli in sotto-problemi fino a giungere al livello del nostro esempio - nell'ipotesi migliore - così che una sola persona possa comprenderne lo svolgimento. Tale approccio si chiama "dividi e conquista", e può essere praticato con ogni linguaggio, anche se con minore - in Pascal - o maggiore - in FORTRAN - difficoltà.